var EXIF = {};
(function() {
	var bDebug = false;

	EXIF.Tags = {

		// version tags
		0x9000 : "ExifVersion",         // EXIF version
		0xA000 : "FlashpixVersion",     // Flashpix format version

		// colorspace tags
		0xA001 : "ColorSpace",          // Color space information tag

		// image configuration
		0xA002 : "PixelXDimension",     // Valid width of meaningful image
		0xA003 : "PixelYDimension",     // Valid height of meaningful image
		0x9101 : "ComponentsConfiguration", // Information about channels
		0x9102 : "CompressedBitsPerPixel",  // Compressed bits per pixel

		// user information
		0x927C : "MakerNote",           // Any desired information written by the manufacturer
		0x9286 : "UserComment",         // Comments by user

		// related file
		0xA004 : "RelatedSoundFile",        // Name of related sound file

		// date and time
		0x9003 : "DateTimeOriginal",        // Date and time when the original image was generated
		0x9004 : "DateTimeDigitized",       // Date and time when the image was stored digitally
		0x9290 : "SubsecTime",          // Fractions of seconds for DateTime
		0x9291 : "SubsecTimeOriginal",      // Fractions of seconds for DateTimeOriginal
		0x9292 : "SubsecTimeDigitized",     // Fractions of seconds for DateTimeDigitized

		// picture-taking conditions
		0x829A : "ExposureTime",        // Exposure time (in seconds)
		0x829D : "FNumber",         // F number
		0x8822 : "ExposureProgram",     // Exposure program
		0x8824 : "SpectralSensitivity",     // Spectral sensitivity
		0x8827 : "ISOSpeedRatings",     // ISO speed rating
		0x8828 : "OECF",            // Optoelectric conversion factor
		0x9201 : "ShutterSpeedValue",       // Shutter speed
		0x9202 : "ApertureValue",       // Lens aperture
		0x9203 : "BrightnessValue",     // Value of brightness
		0x9204 : "ExposureBias",        // Exposure bias
		0x9205 : "MaxApertureValue",        // Smallest F number of lens
		0x9206 : "SubjectDistance",     // Distance to subject in meters
		0x9207 : "MeteringMode",        // Metering mode
		0x9208 : "LightSource",         // Kind of light source
		0x9209 : "Flash",           // Flash status
		0x9214 : "SubjectArea",         // Location and area of main subject
		0x920A : "FocalLength",         // Focal length of the lens in mm
		0xA20B : "FlashEnergy",         // Strobe energy in BCPS
		0xA20C : "SpatialFrequencyResponse",    //
		0xA20E : "FocalPlaneXResolution",   // Number of pixels in width direction per FocalPlaneResolutionUnit
		0xA20F : "FocalPlaneYResolution",   // Number of pixels in height direction per FocalPlaneResolutionUnit
		0xA210 : "FocalPlaneResolutionUnit",    // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution
		0xA214 : "SubjectLocation",     // Location of subject in image
		0xA215 : "ExposureIndex",       // Exposure index selected on camera
		0xA217 : "SensingMethod",       // Image sensor type
		0xA300 : "FileSource",          // Image source (3 == DSC)
		0xA301 : "SceneType",           // Scene type (1 == directly photographed)
		0xA302 : "CFAPattern",          // Color filter array geometric pattern
		0xA401 : "CustomRendered",      // Special processing
		0xA402 : "ExposureMode",        // Exposure mode
		0xA403 : "WhiteBalance",        // 1 = auto white balance, 2 = manual
		0xA404 : "DigitalZoomRation",       // Digital zoom ratio
		0xA405 : "FocalLengthIn35mmFilm",   // Equivalent foacl length assuming 35mm film camera (in mm)
		0xA406 : "SceneCaptureType",        // Type of scene
		0xA407 : "GainControl",         // Degree of overall image gain adjustment
		0xA408 : "Contrast",            // Direction of contrast processing applied by camera
		0xA409 : "Saturation",          // Direction of saturation processing applied by camera
		0xA40A : "Sharpness",           // Direction of sharpness processing applied by camera
		0xA40B : "DeviceSettingDescription",    //
		0xA40C : "SubjectDistanceRange",    // Distance to subject

		// other tags
		0xA005 : "InteroperabilityIFDPointer",
		0xA420 : "ImageUniqueID"        // Identifier assigned uniquely to each image
	};

	EXIF.TiffTags = {
		0x0100 : "ImageWidth",
		0x0101 : "ImageHeight",
		0x8769 : "ExifIFDPointer",
		0x8825 : "GPSInfoIFDPointer",
		0xA005 : "InteroperabilityIFDPointer",
		0x0102 : "BitsPerSample",
		0x0103 : "Compression",
		0x0106 : "PhotometricInterpretation",
		0x0112 : "Orientation",
		0x0115 : "SamplesPerPixel",
		0x011C : "PlanarConfiguration",
		0x0212 : "YCbCrSubSampling",
		0x0213 : "YCbCrPositioning",
		0x011A : "XResolution",
		0x011B : "YResolution",
		0x0128 : "ResolutionUnit",
		0x0111 : "StripOffsets",
		0x0116 : "RowsPerStrip",
		0x0117 : "StripByteCounts",
		0x0201 : "JPEGInterchangeFormat",
		0x0202 : "JPEGInterchangeFormatLength",
		0x012D : "TransferFunction",
		0x013E : "WhitePoint",
		0x013F : "PrimaryChromaticities",
		0x0211 : "YCbCrCoefficients",
		0x0214 : "ReferenceBlackWhite",
		0x0132 : "DateTime",
		0x010E : "ImageDescription",
		0x010F : "Make",
		0x0110 : "Model",
		0x0131 : "Software",
		0x013B : "Artist",
		0x8298 : "Copyright"
	}

	EXIF.GPSTags = {
		0x0000 : "GPSVersionID",
		0x0001 : "GPSLatitudeRef",
		0x0002 : "GPSLatitude",
		0x0003 : "GPSLongitudeRef",
		0x0004 : "GPSLongitude",
		0x0005 : "GPSAltitudeRef",
		0x0006 : "GPSAltitude",
		0x0007 : "GPSTimeStamp",
		0x0008 : "GPSSatellites",
		0x0009 : "GPSStatus",
		0x000A : "GPSMeasureMode",
		0x000B : "GPSDOP",
		0x000C : "GPSSpeedRef",
		0x000D : "GPSSpeed",
		0x000E : "GPSTrackRef",
		0x000F : "GPSTrack",
		0x0010 : "GPSImgDirectionRef",
		0x0011 : "GPSImgDirection",
		0x0012 : "GPSMapDatum",
		0x0013 : "GPSDestLatitudeRef",
		0x0014 : "GPSDestLatitude",
		0x0015 : "GPSDestLongitudeRef",
		0x0016 : "GPSDestLongitude",
		0x0017 : "GPSDestBearingRef",
		0x0018 : "GPSDestBearing",
		0x0019 : "GPSDestDistanceRef",
		0x001A : "GPSDestDistance",
		0x001B : "GPSProcessingMethod",
		0x001C : "GPSAreaInformation",
		0x001D : "GPSDateStamp",
		0x001E : "GPSDifferential"
	}

	EXIF.StringValues = {
		ExposureProgram : {
			0 : "Not defined",
			1 : "Manual",
			2 : "Normal program",
			3 : "Aperture priority",
			4 : "Shutter priority",
			5 : "Creative program",
			6 : "Action program",
			7 : "Portrait mode",
			8 : "Landscape mode"
		},
		MeteringMode : {
			0 : "Unknown",
			1 : "Average",
			2 : "CenterWeightedAverage",
			3 : "Spot",
			4 : "MultiSpot",
			5 : "Pattern",
			6 : "Partial",
			255 : "Other"
		},
		LightSource : {
			0 : "Unknown",
			1 : "Daylight",
			2 : "Fluorescent",
			3 : "Tungsten (incandescent light)",
			4 : "Flash",
			9 : "Fine weather",
			10 : "Cloudy weather",
			11 : "Shade",
			12 : "Daylight fluorescent (D 5700 - 7100K)",
			13 : "Day white fluorescent (N 4600 - 5400K)",
			14 : "Cool white fluorescent (W 3900 - 4500K)",
			15 : "White fluorescent (WW 3200 - 3700K)",
			17 : "Standard light A",
			18 : "Standard light B",
			19 : "Standard light C",
			20 : "D55",
			21 : "D65",
			22 : "D75",
			23 : "D50",
			24 : "ISO studio tungsten",
			255 : "Other"
		},
		Flash : {
			0x0000 : "Flash did not fire",
			0x0001 : "Flash fired",
			0x0005 : "Strobe return light not detected",
			0x0007 : "Strobe return light detected",
			0x0009 : "Flash fired, compulsory flash mode",
			0x000D : "Flash fired, compulsory flash mode, return light not detected",
			0x000F : "Flash fired, compulsory flash mode, return light detected",
			0x0010 : "Flash did not fire, compulsory flash mode",
			0x0018 : "Flash did not fire, auto mode",
			0x0019 : "Flash fired, auto mode",
			0x001D : "Flash fired, auto mode, return light not detected",
			0x001F : "Flash fired, auto mode, return light detected",
			0x0020 : "No flash function",
			0x0041 : "Flash fired, red-eye reduction mode",
			0x0045 : "Flash fired, red-eye reduction mode, return light not detected",
			0x0047 : "Flash fired, red-eye reduction mode, return light detected",
			0x0049 : "Flash fired, compulsory flash mode, red-eye reduction mode",
			0x004D : "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",
			0x004F : "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",
			0x0059 : "Flash fired, auto mode, red-eye reduction mode",
			0x005D : "Flash fired, auto mode, return light not detected, red-eye reduction mode",
			0x005F : "Flash fired, auto mode, return light detected, red-eye reduction mode"
		},
		SensingMethod : {
			1 : "Not defined",
			2 : "One-chip color area sensor",
			3 : "Two-chip color area sensor",
			4 : "Three-chip color area sensor",
			5 : "Color sequential area sensor",
			7 : "Trilinear sensor",
			8 : "Color sequential linear sensor"
		},
		SceneCaptureType : {
			0 : "Standard",
			1 : "Landscape",
			2 : "Portrait",
			3 : "Night scene"
		},
		SceneType : {
			1 : "Directly photographed"
		},
		CustomRendered : {
			0 : "Normal process",
			1 : "Custom process"
		},
		WhiteBalance : {
			0 : "Auto white balance",
			1 : "Manual white balance"
		},
		GainControl : {
			0 : "None",
			1 : "Low gain up",
			2 : "High gain up",
			3 : "Low gain down",
			4 : "High gain down"
		},
		Contrast : {
			0 : "Normal",
			1 : "Soft",
			2 : "Hard"
		},
		Saturation : {
			0 : "Normal",
			1 : "Low saturation",
			2 : "High saturation"
		},
		Sharpness : {
			0 : "Normal",
			1 : "Soft",
			2 : "Hard"
		},
		SubjectDistanceRange : {
			0 : "Unknown",
			1 : "Macro",
			2 : "Close view",
			3 : "Distant view"
		},
		FileSource : {
			3 : "DSC"
		},

		Components : {
			0 : "",
			1 : "Y",
			2 : "Cb",
			3 : "Cr",
			4 : "R",
			5 : "G",
			6 : "B"
		}
	}

	function addEvent(oElement, strEvent, fncHandler)
	{
		if (oElement.addEventListener) {
			oElement.addEventListener(strEvent, fncHandler, false);
		} else if (oElement.attachEvent) {
			oElement.attachEvent("on" + strEvent, fncHandler);
		}
	}


	function imageHasData(oImg)
	{
		return !!(oImg.exifdata);
	}

	function getImageData(oImg, fncCallback)
	{
		BinaryAjax(
			oImg.src,
			function(oHTTP) {
				var oEXIF = findEXIFinJPEG(oHTTP.binaryResponse);
				oImg.exifdata = oEXIF || {};
				if (fncCallback) fncCallback();
			}
		)
	}

	function findEXIFinJPEG(oFile) {
		var aMarkers = [];

		if (oFile.getByteAt(0) != 0xFF || oFile.getByteAt(1) != 0xD8) {
			return false; // not a valid jpeg
		}

		var iOffset = 2;
		var iLength = oFile.getLength();
		while (iOffset < iLength) {
			if (oFile.getByteAt(iOffset) != 0xFF) {
				if (bDebug) console.log("Not a valid marker at offset " + iOffset + ", found: " + oFile.getByteAt(iOffset));
				return false; // not a valid marker, something is wrong
			}

			var iMarker = oFile.getByteAt(iOffset+1);

			// we could implement handling for other markers here,
			// but we're only looking for 0xFFE1 for EXIF data

			if (iMarker == 22400) {
				if (bDebug) console.log("Found 0xFFE1 marker");
				return readEXIFData(oFile, iOffset + 4, oFile.getShortAt(iOffset+2, true)-2);
				iOffset += 2 + oFile.getShortAt(iOffset+2, true);

			} else if (iMarker == 225) {
				// 0xE1 = Application-specific 1 (for EXIF)
				if (bDebug) console.log("Found 0xFFE1 marker");
				return readEXIFData(oFile, iOffset + 4, oFile.getShortAt(iOffset+2, true)-2);

			} else {
				iOffset += 2 + oFile.getShortAt(iOffset+2, true);
			}

		}

	}


	function readTags(oFile, iTIFFStart, iDirStart, oStrings, bBigEnd)
	{
		var iEntries = oFile.getShortAt(iDirStart, bBigEnd);
		var oTags = {};
		for (var i=0;i<iEntries;i++) {
			var iEntryOffset = iDirStart + i*12 + 2;
			var strTag = oStrings[oFile.getShortAt(iEntryOffset, bBigEnd)];
			if (!strTag && bDebug) console.log("Unknown tag: " + oFile.getShortAt(iEntryOffset, bBigEnd));
			oTags[strTag] = readTagValue(oFile, iEntryOffset, iTIFFStart, iDirStart, bBigEnd);
		}
		return oTags;
	}


	function readTagValue(oFile, iEntryOffset, iTIFFStart, iDirStart, bBigEnd)
	{
		var iType = oFile.getShortAt(iEntryOffset+2, bBigEnd);
		var iNumValues = oFile.getLongAt(iEntryOffset+4, bBigEnd);
		var iValueOffset = oFile.getLongAt(iEntryOffset+8, bBigEnd) + iTIFFStart;

		switch (iType) {
			case 1: // byte, 8-bit unsigned int
			case 7: // undefined, 8-bit byte, value depending on field
				if (iNumValues == 1) {
					return oFile.getByteAt(iEntryOffset + 8, bBigEnd);
				} else {
					var iValOffset = iNumValues > 4 ? iValueOffset : (iEntryOffset + 8);
					var aVals = [];
					for (var n=0;n<iNumValues;n++) {
						aVals[n] = oFile.getByteAt(iValOffset + n);
					}
					return aVals;
				}
				break;

			case 2: // ascii, 8-bit byte
				var iStringOffset = iNumValues > 4 ? iValueOffset : (iEntryOffset + 8);
				return oFile.getStringAt(iStringOffset, iNumValues-1);
				break;

			case 3: // short, 16 bit int
				if (iNumValues == 1) {
					return oFile.getShortAt(iEntryOffset + 8, bBigEnd);
				} else {
					var iValOffset = iNumValues > 2 ? iValueOffset : (iEntryOffset + 8);
					var aVals = [];
					for (var n=0;n<iNumValues;n++) {
						aVals[n] = oFile.getShortAt(iValOffset + 2*n, bBigEnd);
					}
					return aVals;
				}
				break;

			case 4: // long, 32 bit int
				if (iNumValues == 1) {
					return oFile.getLongAt(iEntryOffset + 8, bBigEnd);
				} else {
					var aVals = [];
					for (var n=0;n<iNumValues;n++) {
						aVals[n] = oFile.getLongAt(iValueOffset + 4*n, bBigEnd);
					}
					return aVals;
				}
				break;
			case 5: // rational = two long values, first is numerator, second is denominator
				if (iNumValues == 1) {
					return oFile.getLongAt(iValueOffset, bBigEnd) / oFile.getLongAt(iValueOffset+4, bBigEnd);
				} else {
					var aVals = [];
					for (var n=0;n<iNumValues;n++) {
						aVals[n] = oFile.getLongAt(iValueOffset + 8*n, bBigEnd) / oFile.getLongAt(iValueOffset+4 + 8*n, bBigEnd);
					}
					return aVals;
				}
				break;
			case 9: // slong, 32 bit signed int
				if (iNumValues == 1) {
					return oFile.getSLongAt(iEntryOffset + 8, bBigEnd);
				} else {
					var aVals = [];
					for (var n=0;n<iNumValues;n++) {
						aVals[n] = oFile.getSLongAt(iValueOffset + 4*n, bBigEnd);
					}
					return aVals;
				}
				break;
			case 10: // signed rational, two slongs, first is numerator, second is denominator
				if (iNumValues == 1) {
					return oFile.getSLongAt(iValueOffset, bBigEnd) / oFile.getSLongAt(iValueOffset+4, bBigEnd);
				} else {
					var aVals = [];
					for (var n=0;n<iNumValues;n++) {
						aVals[n] = oFile.getSLongAt(iValueOffset + 8*n, bBigEnd) / oFile.getSLongAt(iValueOffset+4 + 8*n, bBigEnd);
					}
					return aVals;
				}
				break;
		}
	}


	function readEXIFData(oFile, iStart, iLength)
	{
		if (oFile.getStringAt(iStart, 4) != "Exif") {
			if (bDebug) console.log("Not valid EXIF data! " + oFile.getStringAt(iStart, 4));
			return false;
		}

		var bBigEnd;

		var iTIFFOffset = iStart + 6;

		// test for TIFF validity and endianness
		if (oFile.getShortAt(iTIFFOffset) == 0x4949) {
			bBigEnd = false;
		} else if (oFile.getShortAt(iTIFFOffset) == 0x4D4D) {
			bBigEnd = true;
		} else {
			if (bDebug) console.log("Not valid TIFF data! (no 0x4949 or 0x4D4D)");
			return false;
		}

		if (oFile.getShortAt(iTIFFOffset+2, bBigEnd) != 0x002A) {
			if (bDebug) console.log("Not valid TIFF data! (no 0x002A)");
			return false;
		}

		if (oFile.getLongAt(iTIFFOffset+4, bBigEnd) != 0x00000008) {
			if (bDebug) console.log("Not valid TIFF data! (First offset not 8)", oFile.getShortAt(iTIFFOffset+4, bBigEnd));
			return false;
		}

		var oTags = readTags(oFile, iTIFFOffset, iTIFFOffset+8, EXIF.TiffTags, bBigEnd);

		if (oTags.ExifIFDPointer) {
			var oEXIFTags = readTags(oFile, iTIFFOffset, iTIFFOffset + oTags.ExifIFDPointer, EXIF.Tags, bBigEnd);
			for (var strTag in oEXIFTags) {
				switch (strTag) {
					case "LightSource" :
					case "Flash" :
					case "MeteringMode" :
					case "ExposureProgram" :
					case "SensingMethod" :
					case "SceneCaptureType" :
					case "SceneType" :
					case "CustomRendered" :
					case "WhiteBalance" :
					case "GainControl" :
					case "Contrast" :
					case "Saturation" :
					case "Sharpness" :
					case "SubjectDistanceRange" :
					case "FileSource" :
						oEXIFTags[strTag] = EXIF.StringValues[strTag][oEXIFTags[strTag]];
						break;

					case "ExifVersion" :
					case "FlashpixVersion" :
						oEXIFTags[strTag] = String.fromCharCode(oEXIFTags[strTag][0], oEXIFTags[strTag][1], oEXIFTags[strTag][2], oEXIFTags[strTag][3]);
						break;

					case "ComponentsConfiguration" :
						oEXIFTags[strTag] =
							EXIF.StringValues.Components[oEXIFTags[strTag][0]]
							+ EXIF.StringValues.Components[oEXIFTags[strTag][1]]
							+ EXIF.StringValues.Components[oEXIFTags[strTag][2]]
							+ EXIF.StringValues.Components[oEXIFTags[strTag][3]];
						break;
				}
				oTags[strTag] = oEXIFTags[strTag];
			}
		}

		if (oTags.GPSInfoIFDPointer) {
			var oGPSTags = readTags(oFile, iTIFFOffset, iTIFFOffset + oTags.GPSInfoIFDPointer, EXIF.GPSTags, bBigEnd);
			for (var strTag in oGPSTags) {
				switch (strTag) {
					case "GPSVersionID" :
						oGPSTags[strTag] = oGPSTags[strTag][0]
							+ "." + oGPSTags[strTag][1]
							+ "." + oGPSTags[strTag][2]
							+ "." + oGPSTags[strTag][3];
						break;
				}
				oTags[strTag] = oGPSTags[strTag];
			}
		}

		return oTags;
	}


	EXIF.getData = function(oImg, fncCallback)
	{
		if (!oImg.complete) return false;
		if (!imageHasData(oImg)) {
			getImageData(oImg, fncCallback);
		} else {
			if (fncCallback) fncCallback();
		}
		return true;
	}

	EXIF.getTag = function(oImg, strTag)
	{
		if (!imageHasData(oImg)) return;
		return oImg.exifdata[strTag];
	}

	EXIF.getAllTags = function(oImg)
	{
		if (!imageHasData(oImg)) return {};
		var oData = oImg.exifdata;
		var oAllTags = {};
		for (var a in oData) {
			if (oData.hasOwnProperty(a)) {
				oAllTags[a] = oData[a];
			}
		}
		return oAllTags;
	}


	EXIF.pretty = function(oImg)
	{
		if (!imageHasData(oImg)) return "";
		var oData = oImg.exifdata;
		var strPretty = "";
		for (var a in oData) {
			if (oData.hasOwnProperty(a)) {
				if (typeof oData[a] == "object") {
					strPretty += a + " : [" + oData[a].length + " values]\r\n";
				} else {
					strPretty += a + " : " + oData[a] + "\r\n";
				}
			}
		}
		return strPretty;
	}

	EXIF.readFromBinaryFile = function(oFile) {
		return findEXIFinJPEG(oFile);
	}

	function loadAllImages()
	{
		var aImages = document.getElementsByTagName("img");
		for (var i=0;i<aImages.length;i++) {
			if (aImages[i].getAttribute("exif") == "true") {
				if (!aImages[i].complete) {
					addEvent(aImages[i], "load",
						function() {
							EXIF.getData(this);
						}
					);
				} else {
					EXIF.getData(aImages[i]);
				}
			}
		}
	}

	addEvent(window, "load", loadAllImages);
})();